﻿#region License
/*
Copyright (c) 2009, G.W. van der Vegt
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided 
that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this list of conditions and the 
  following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and 
  the following disclaimer in the documentation and/or other materials provided with the distribution.

* Neither the name of G.W. van der Vegt nor the names of its contributors may be 
  used to endorse or promote products derived from this software without specific prior written 
  permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 
THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
*/
#endregion License

#region changelog

//----------   ---   -------------------------------------------------------------------------------
//Purpose:           Support for Compound Document API in Transacted Mode.
//By:                G.W. van der Vegt (wvd_vegt@knoware.nl)
//Url:               http://swiss.codeplex.com
//Depends:           Storage
//License:           New BSD License
//----------   ---   -------------------------------------------------------------------------------
//dd-mm-yyyy - who - description
//----------   ---   -------------------------------------------------------------------------------
//dd-mm-yyyy   veg - Created.
//                 - Corrected a bug in StoragePath with Data\ as path.
//                 - Now both Data and Data\ are supported. 
//                 - Files must have an extension as we test for the '.' to distinguish files and directories.
//                 - Turned StoragePath into an IDisposable.
//                 - Replace all int by Int32.
//                 - Moved S_OK into STG enum.
//                 - Added Documentation stubs.
//12-12-2009 - veg - Added _SetFileName like used in IniFile.
//             veg - Corrected FileName behaviour of constructor. 
//                   If no path is passed, the file is created in a directory under %APPDATA%.
//                   The directory is either named after the Application Title or, 
//                   if absent, the Application Executable Name.
//                 - Made IDisposable.
//04-1202010 - veg - Finally added SwitchToFileSystem implementation.
//----------   ---   -------------------------------------------------------------------------------
//dd-mm-yyyy         todo's
//                 - Tie together with IniFile into StorageIniFile: 
//                     a) subclass IniFile and override SaveToFile/LoadFromFile/UpdateFile) or 
//                     b) use a separate class with a public IniFile property.
//                     See Data.cs.
//                 - Create Streams and Storages on demand.
//                 - Migrate other functions from ITStorageWrapper.
//                 - Documentation.
//                 - Add IRootStorage with SwitchToFile() method.
//                 - Add Reference Counting (AddRef & Release to get the actual reference count).
//----------   ---   -------------------------------------------------------------------------------

#endregion changelog

namespace Swiss
{
    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Reflection;
    using System.Runtime.InteropServices;
    using System.Windows.Forms;
    using System.Collections.Specialized;

    /// <summary>
    /// 
    /// </summary>
    public class Storage : IDisposable
    {
        // private IntPtr snbExclude = (IntPtr)0;

        /// <summary>
        /// The FileName of the Storage to be Opened.
        /// </summary>
        public string FileName
        {
            get
            {
                return fFileName;
            }
            //set
            //{
            //    fFileName = value;
            //}
        }
        private String fFileName = "";

        /// <summary>
        /// The Root Storage of the File to be Opened.
        /// </summary>
        public Interop.IStorage Root
        {
            get
            {
                return fRoot;
            }
        }
        private Interop.IStorage fRoot = null;

        /// <summary>
        /// Returns true of the Storage is Open.
        /// </summary>
        public Boolean Active
        {
            get
            {
                return Assigned(fRoot);
            }
            set
            {
                if (Active != value)
                {
                    switch (value)
                    {
                        case true:
                            Open();
                            break;
                        case false:
                            Close();
                            break;
                    }
                }
            }
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        public Storage()
            : this("")
        {
            //Nothing Else
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="aFilename">The Filename of the Storage to open.</param>
        public Storage(string aFilename)
            : this(aFilename, false)
        {
            //Nothing
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="aFilename">The Filename of the Storage to open.</param>
        /// <param name="doOpen">If false only the filename is set and the called must explicitly open the storage with a call of Storage.Open().</param>
        public Storage(string aFilename, Boolean doOpen)
        {
            fRoot = null;
            fFileName = "";

            if (doOpen)
            {
                Open(aFilename);
            }
            else
            {
                _SetFileName(aFilename);
            }
        }

        /// <summary>
        /// Helper to Mimic Delphi's Assigned function.
        /// </summary>
        /// <param name="AObject">True if the Object is assigned.</param>
        /// <returns></returns>
        private bool Assigned(object AObject)
        {
            return (AObject != null);
        }

        internal void _SetFileName(String IniFileName)
        {
            String IniPath = IniFileName;
            String AppDir = Path.GetDirectoryName(Application.ExecutablePath);

            if (IniPath == Path.GetFileName(IniPath))
            {
                String AppData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);

                String Apptitle = ((AssemblyTitleAttribute)AssemblyTitleAttribute.GetCustomAttribute(Assembly.GetExecutingAssembly(), typeof(AssemblyTitleAttribute))).Title;
                if (String.IsNullOrEmpty(Apptitle))
                {
                    String[] split = AppDir.Split(Path.DirectorySeparatorChar);
                    Apptitle = Path.ChangeExtension(split[split.Length - 1], "");
                }

                fFileName = Path.Combine(Path.Combine(AppData, Apptitle), IniPath);
            }
            else
            {
                fFileName = Path.GetFullPath(IniPath);
            }
        }

        /// <summary>
        /// Opens or Creates the Structured Storage from Disk.
        /// </summary>
        /// <param name="aFileName">The FileName of the Storage to Open.</param>
        /// <returns></returns>
        public Boolean Open(string aFileName)
        {
            _SetFileName(aFileName);

            return Open();
        }

        /// <summary>
        /// Opens or Creates the Structured Storage from Disk.
        /// 
        /// This libary works in transacted mode so commits are 
        /// neccesary for actually storing something.
        /// </summary>
        /// <returns>True if succesfully opened.</returns>
        public Boolean Open()
        {
            Int32 hres = (Int32)Interop.STG.S_OK;

            //Make sure the directory exists before attempting to access it.
            if (!Directory.Exists(Path.GetDirectoryName(fFileName)))
            {
                Directory.CreateDirectory(Path.GetDirectoryName(fFileName));
            }

            if (Interop.StgIsStorageFile(FileName) != (Int32)Interop.STG.E_FILENOTFOUND)
            {
                hres = Interop.StgOpenStorage(
                     FileName,
                     null,       //pstgPriority
                     Interop.STGM.TRANSACTED | Interop.STGM.READWRITE | Interop.STGM.SHARE_EXCLUSIVE,
                     (IntPtr)0,  //snbExclude
                     0,          //reserved
                     out fRoot);
            }
            else
            {
                hres = Interop.StgCreateDocfile(
                    FileName,
                    Interop.STGM.FAILIFTHERE | Interop.STGM.TRANSACTED | Interop.STGM.READWRITE | Interop.STGM.SHARE_DENY_WRITE,
                    0,          //reserved
                    out fRoot);
            }

            return Assigned(fRoot) & (hres == (Int32)Interop.STG.S_OK);
        }

        /// <summary>
        /// This libary works in transacted mode so commits are 
        /// neccesary for actually storing something.
        /// </summary>
        public void Commit()
        {
            Commit(Interop.STGC.DEFAULT);
        }

        /// <summary>
        /// This libary works in transacted mode so commits are 
        /// neccesary for actually storing something.
        /// </summary>
        /// <param name="grfCommitFlags"></param>
        public void Commit(Interop.STGC grfCommitFlags)
        {
            Debug.WriteLine("OleStorage.Commit()");

            fRoot.Commit((Int32)grfCommitFlags);
        }

        /// <summary>
        /// Close the Storage.
        /// </summary>
        /// <returns>True if the Storage </returns>
        public Boolean Close()
        {
            Dispose();

            GC.Collect();

            return !Assigned(fRoot);
        }

        /// <summary>
        /// 
        /// </summary>
        public Int32 RefCount
        {
            get
            {
                if (Assigned(fRoot))
                {
                    return Marshal.Release(Marshal.GetIUnknownForObject(fRoot)) - 1; //Returns oldreference Count so -1!
                }
                else
                {
                    return 0;
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public void Dispose()
        {
            Debug.WriteLine("OleStorage.Dispose()");

            //The Following code interferes with proper storing...
            if (Assigned(fRoot))
            {
                Debug.WriteLine("Root Reference Count: " + RefCount.ToString()); //is 2, where is the other comming from?

                Marshal.ReleaseComObject(fRoot);
            }
        }

        /// <summary>
        /// Checks if a storage exists.
        /// </summary>
        /// <param name="aPath">The path of the storage to be checked for existance.</param>
        /// <returns>True if the storage exists, else false.</returns>
        public Boolean StorageExists(string aPath)
        {
            //Remove Trailing Backslash
            //TODO: Make it into PathDelimiter..
            if (aPath.EndsWith(Path.DirectorySeparatorChar.ToString()))
            {
                aPath = aPath.Substring(0, aPath.Length - 1);
            }

            if (aPath.Length != 0)
            {
                StoragePath aStoragePath = new StoragePath(aPath, Root);

                //Note that when there is no trailing path delimiter
                //the last part of the path will be treated as a filename
                //so no Leaf Storage was created there and we can safely test.
                String fPath = StoragePath.ExtractFileName(aPath);
                Boolean Result = aStoragePath.StorageExists(fPath);
                aStoragePath.Dispose();

                return Result;
            }
            else
            {
                return true;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="aStream"></param>
        /// <returns></returns>
        public Boolean StreamExists(string aStream)
        {
            if (StorageExists(StoragePath.ExtractFilePath(aStream)))
            {
                StoragePath aStoragePath = new StoragePath(aStream, Root);

                Boolean Result = aStoragePath.StreamExists(StoragePath.ExtractFileName(aStream));
                aStoragePath.Dispose();

                return Result;
            }
            else
            {
                return false;
            }
        }

        /// <summary>
        /// Extracts all storages and streams as directories and files to the harddisk.
        /// 
        /// A Directory is created with the FileName of the Storage.
        /// </summary>
        public void SwitchToFileSystem()
        {
            InternalSwitchToFileSystem("\\");
        }

        /// <summary>
        /// Helper for SwitchToFileSystem().
        /// 
        /// It enumerates Streams & Storages and writes them to disk. 
        /// 
        /// For storages recursion is used to traverse the whole Storage.
        /// 
        /// Special Streams (starting with characters between 0 and 31) are ommitted.
        /// </summary>
        /// <param name="path"></param>
        private void InternalSwitchToFileSystem(String path)
        {
            String prefix = Path.Combine(Path.ChangeExtension(FileName, ""), path.TrimStart('\\'));

            if (!Directory.Exists(prefix))
            {
                Directory.CreateDirectory(prefix);
            }

            StringCollection storages;

            using (StoragePath sp = new StoragePath(path, Root))
            {
                storages = sp.StorageListing(Interop.STGTY.STREAM);
            }

            foreach (String s in storages)
            {
                //Skip Special Streams (s[0] in [0..31])...
                //s.chStartsWith("") works (05)
                if (s.Length != 0 && s[0] < 32)
                {
                    continue;
                }

                using (StorageStream ss = new StorageStream(Path.Combine(path, s), Root))
                {
                    File.WriteAllText(Path.Combine(prefix, s), ss.Text);
                }
            }

            using (StoragePath sp = new StoragePath(path, Root))
            {
                storages = sp.StorageListing(Interop.STGTY.STORAGE);
            }

            foreach (String s in storages)
            {
                if (!Directory.Exists(Path.Combine(prefix, s)))
                {
                    Directory.CreateDirectory(Path.Combine(prefix, s));
                }

                //Recurse for Storages.
                InternalSwitchToFileSystem(Path.Combine(path, s));
            }
        }

        #region IDisposable Members

        /// <summary>
        /// 
        /// </summary>
        void IDisposable.Dispose()
        {
            //Call normal Dispose().
            Dispose();
        }

        #endregion
    }
}